package com.me.study.javaCore.io;

import sun.misc.Cleaner;
import sun.nio.ch.DirectBuffer;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.Duration;
import java.time.LocalTime;
import java.util.Arrays;
import java.util.Objects;

/**
 * NIO 测试类：SATA SSD <p></p>
 */
public class NIOTest {

    public static void main(String[] args) {
        // API 测试
        // testPutAndGet();
        // testCompactAndClear();
        // testMarkAndReset();
        // testRewind();


        int count = 1000;
        String readFile = "testJDK8/target/classes/test-file.txt";
        String writeFile = "testJDK8/target/classes/test-file-copy.txt";
        LocalTime now = LocalTime.now();
        for (int i = 0; i < count; i++) {
            readAndWriteByTransfer(readFile, writeFile);
        }
        System.out.println("readAndWriteByTransfer cost time: " + Duration.between(now, LocalTime.now()).toMillis());

    }

    /**
     * 直接缓冲区的模式 完成数据读写：1000次耗时 2189 ms、2235 ms、2250 ms   <p>
     *  transferFrom/transferTo 底层还是使用 DirectByteBuffer  <p></p>
     *
     * @param readFile ：需要读取的文件路径
     * @param writeFile：写入的文件路径
     */
    public static void readAndWriteByTransfer(String readFile, String writeFile) {
        // TODO FileChannel.close() 会自动关闭 FileInputStream.close()
        try (FileChannel inChannel = new FileInputStream(readFile).getChannel();
             FileChannel outChannel = new FileOutputStream(writeFile).getChannel()) {
            inChannel.transferTo(0, inChannel.size(), outChannel);
            // outChannel.transferFrom(inChannel, 0, inChannel.size());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 内存映射 方式二：writeChannel.write(mappedByteBuffer)
     * 1000次耗时 4506ms 4500ms
     */
    public static void readAndWriteByMappedBuffer(String readFilePath, String writeFilePath) {
        FileChannel readChannel = null;
        MappedByteBuffer inMappedBuf = null;
        try {
            // 构建 FileChannel 方式一
            readChannel = new FileInputStream(readFilePath).getChannel();
            WritableByteChannel writableByteChannel = Channels.newChannel(new FileOutputStream(writeFilePath));

            // 实际类型是 DirectByteBuffer：基于 直接内存 的字节缓冲区
            // 关闭通道对映射的有效性没有影响
            inMappedBuf = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, readChannel.size());

            // 内存映射 方式二：TODO 有毒，文件为什么写了 2次
            writableByteChannel.write(inMappedBuf);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (Objects.nonNull(inMappedBuf)) {
                unmap(inMappedBuf);
            }
            try {
                if (Objects.nonNull(readChannel)) {
                    readChannel.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 内存映射（基于 直接内存的）字节缓冲区 完成文件复制：
     * outMappedBuf.put(inMappedBuf)
     *
     * @param readFilePath ：需要读取的文件路径
     * @param writeFilePath：写入的文件路径
     */
    public static void readAndWriteByMappedBuffer222(String readFilePath, String writeFilePath) {
        FileChannel readChannel = null;
        FileChannel writeChannel = null;
        MappedByteBuffer inMappedBuf = null;
        try {

            // 构建 FileChannel 方式一（配合内存映射一、二）：循环 100次 14034 14154 14049
            // TODO Paths.get() 以 `user.dir` 为根路径，本地项目中表示根目录。Tomcat 中 表示的 tomcat根目录。
            //  并且 Junit 以当前模块为根目录，main 是项目根目录。
            readChannel = FileChannel.open(Paths.get(readFilePath), StandardOpenOption.READ);
            writeChannel = FileChannel.open(Paths.get(writeFilePath), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);

            // 构建 FileChannel 方式二（配合内存映射一、二）：循环 100次 14167 14133 14279
            // readChannel = new RandomAccessFile(readFilePath, "r").getChannel();
            // writeChannel = new RandomAccessFile(writeFilePath, "rw").getChannel();

            // 实际类型是 DirectByteBuffer：基于 直接内存 的字节缓冲区
            inMappedBuf = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, readChannel.size());
            MappedByteBuffer outMappedBuf = writeChannel.map(FileChannel.MapMode.READ_WRITE, 0, readChannel.size());

            // 内存映射 方式一：
            // while (inMappedBuf.hasRemaining()) {
            //     outMappedBuf.put(inMappedBuf.get());
            // }
            outMappedBuf.put(inMappedBuf);// 等价于 while (inMappedBuf.hasRemaining())
            outMappedBuf.force();

            // 内存映射 方式二：
            // writeChannel.write(inMappedBuf);
            // writeChannel.force(true);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (Objects.nonNull(inMappedBuf)) {
                unmap(inMappedBuf);
            }
            try {
                if (Objects.nonNull(readChannel)) {
                    readChannel.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (Objects.nonNull(writeChannel)) {
                    writeChannel.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * （nio.FileChannel 基本使用）基于 堆内存的 字节缓冲区的 文件复制：1000次耗时 30986 ms
     *
     * @param readFile ：需要读取的文件路径
     * @param writeFile：写入的文件路径
     */
    public static void readAndWriteByByteBuffer(String readFile, String writeFile) {
        // TODO FileChannel.close() 会自动关闭 FileInputStream.close()
        try (FileChannel inChannel = new FileInputStream(readFile).getChannel();
                 FileChannel outChannel = new FileOutputStream(writeFile).getChannel()) {
            // DirectByteBuffer：基于 直接内存 的字节缓冲区
            // ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
            // HeapByteBuffer：基于 堆内存 的字节缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);

            // 从 inChannel 读取到 buffer，对于 buffer来说是写入
            byte[] bytes = new byte[1024];
            int len;
            while ((len = inChannel.read(buffer)) != -1) {
                // flip() 将 ByteBuffer 切换为 读取模式，并将 limit 设置为 position 的当前位置，然后重置 position
                buffer.flip();

                // 输出文件内容
                // buffer.mark();
                // buffer.get(bytes, 0 , len);
                // System.out.println(new String(bytes, 0 , len));
                // buffer.reset();

                // 读取 buffer 并写入到 outChannel
                outChannel.write(buffer);

                // clear() 切换为写入模式，使 ByteBuffer 可以写入数据
                buffer.clear();
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 关闭内存映射
     *
     * @param mappedByteBuffer
     */
    public static void unmap(MappedByteBuffer mappedByteBuffer) {
        Cleaner cleaner = ((DirectBuffer) mappedByteBuffer).cleaner();
        if (cleaner != null) {
            cleaner.clean();
        }
    }

    public static void testPutAndGet() {
        byte[] byteArr1 = new byte[]{11, 22, 33};
        // 初始化一个大小为 6 的ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(6);
        // 初始状态：position: 0, limit: 6, capacity: 6
        print(buffer);

        // 往buffer中写入3个字节的数据
        buffer.put(byteArr1);
        // 写入之后的状态：position: 3, limit: 6, capacity: 6
        print(buffer);

        System.out.println("\n************** after flip **************");
        // TODO 切换为读取模式，limit设置为当前的 position，并重置 mark 为 -1
        buffer.flip();
        // 切换为读取模式之后的状态：position: 0, limit: 3, capacity: 6
        print(buffer);

        System.out.println("buffer 第1位：" + buffer.get());
        System.out.println("buffer 第2位：" + buffer.get());
        // 读取两个数据之后的状态：position: 2, limit: 3, capacity: 6
        print(buffer);
    }

    /**
     * compact()：读取模式下，压缩空间，并进入写入模式。注意！！！ limit 未重置，容量不满哦
     * clear()：彻底重置，除了数据没清理，其他都跟 new 一样
     * 通常情况下，更多的是使用 compact()，而不是 clear()
     */
    public static void testCompactAndClear() {
        byte[] byteArr = new byte[]{11, 12, 13, 14, 15, 16};
        ByteBuffer buffer = ByteBuffer.allocate(6);
        buffer.put(byteArr);

        // 切换为读取模式，limit设置为当前的 position，并重置 mark 为 -1
        buffer.flip();
        System.out.println("buffer 第1位：" + buffer.get());
        System.out.println("buffer 第2位：" + buffer.get());
        System.out.println("buffer 第3位：" + buffer.get());
        System.out.println("buffer 第4位：" + buffer.get());
        buffer.flip();
        // 读取模式下，压缩空间并设置为写入模式，
        buffer.compact();
        buffer.put((byte) 29);
        buffer.put((byte) 28);
        // TODO 因为 compact 不重置 limit，所以写入的上限为 4，多写会报错
        // buffer.put((byte) 27);
        buffer.flip();
        // 读完指针就跑到最后了：position: 4, limit: 4, capacity: 6
        System.out.println(Arrays.toString(buffer.get(new byte[2]).array()));
        // System.out.println(Arrays.toString(buffer.get(new byte[6], 0, 2).array()));
        print(buffer);

        buffer.clear();
        buffer.put((byte) 35);
        buffer.put((byte) 36);
        buffer.flip();
        System.out.println(Arrays.toString(buffer.get(new byte[2]).array()));
    }

    /**
     * reset()：把 position 重置到 mark处，limit 未重置，可以用来实现 重复读取
     */
    public static void testMarkAndReset() {
        byte[] byteArr = new byte[]{37, 38, 39};
        // 初始化一个大小为 6 的ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(6);

        // 写入3个字节的数据
        buffer.put((byte) 11);
        buffer.mark();
        buffer.put((byte) 12);
        buffer.put((byte) 13);

        // 写入模式下重置，表示从 mark 处重新写入
        buffer.reset();
        buffer.put(byteArr);

        // 切换为读取模式，limit设置为当前 position（4），并重置 mark 为 -1
        buffer.flip();

        System.out.println("buffer 第1位：" + buffer.get());
        buffer.mark();
        System.out.println("buffer 第2位：" + buffer.get());
        System.out.println("buffer 第3位：" + buffer.get());
        // 读取模式下重置，表示从 mark 处重新读取
        buffer.reset();
        System.out.println("reset 后第一次读：" + buffer.get());
    }

    /**
     * rewind 彻底重置，mark 重置为 -1，position 重置为 0，limit 未重置
     */
    public static void testRewind() {
        byte[] byteArr = new byte[]{'l', 'm', 'n'};
        byte[] dstArr = new byte[2];
        // 初始化一个大小为 6 的ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(6);

        // 写入3个字节的数据
        buffer.put((byte) 11);
        buffer.mark();
        buffer.put((byte) 22);
        buffer.put((byte) 33);

        // TODO 写入模式下重置，表示从头开始重新写入，并重置 mark 为 -1
        buffer.rewind();
        buffer.put(byteArr);

        buffer.flip();
        System.out.println(Arrays.toString(buffer.get(dstArr).array()));
    }

    private static void print(ByteBuffer buffer) {
        System.out.printf("position: %d, limit: %d, capacity: %d\n",
                buffer.position(), buffer.limit(), buffer.capacity());
    }

}
